package net.fdukedom.epicurus.plugin;

import net.fdukedom.epicurus.domain.entity.*;
import net.fdukedom.epicurus.domain.util.PluginHelper;
import org.jdom.Attribute;
import org.jdom.Element;

import java.util.*;

/**
 * Loader for plug-ins.
 *
 * @author Dmitry Y. Kochelaev
 */
public abstract class AbstractPluginLoader {

    /**
     * Already loaded groups.
     */
    protected Map<String, Group> groups = new HashMap<String, Group>();

    /**
     * Loads all plug-ins. If plugin is not created in system then entry for this
     * plugin is added to corresponding table.
     *
     * @return plug-ins' list
     */
    public abstract List<Plugin> loadPlugins();

    /**
     * Parses string value from element.
     *
     * @param element element to be parsed
     *
     * @return string value of element
     *
     * @throws PluginParsingException if elemnt is null
     */
    private String parseStringValue(Element element) throws PluginParsingException {
        if (element == null) {
            throw new PluginParsingException();
        }
        return element.getTextTrim();
    }

    /**
     * Parses integer value from element.
     *
     * @param element element to be parsed
     *
     * @return integer value of element
     *
     * @throws PluginParsingException if element is null or contains non-integer value
     */
    private int parseIntValue(Element element) throws PluginParsingException {
        int value;
        try {
            value = Integer.parseInt(parseStringValue(element));
        } catch (NumberFormatException e) {
            throw new PluginParsingException(e);
        }
        return value;
    }

    /**
     * Parses plugin from XML-document.
     *
     * @param root root element of plug-in description
     *
     * @param registredPlugins plug-ins registred in system
     *
     * @return list of plug-ins
     *
     * @throws PluginParsingException if an error occured
     */
    protected Plugin parsePlugin(Element root, Map<String, Plugin> registredPlugins) throws PluginParsingException {
        if (!"plugin".equals(root.getName())) {
            throw new PluginParsingException();
        }

        Plugin plugin;

        String name = parseStringValue(root.getChild("name"));
        int version = parseIntValue(root.getChild("version"));
        if (registredPlugins.containsKey(name)) {
            plugin = registredPlugins.get(name);
            if (plugin.getVersion() == version) {
                //It's the same version of plugin. There is no sence in further actions.
                for (Group group : plugin.getGroups()) {
                    groups.put(group.getName(), group);
                }
                return plugin;
            } else {
                plugin.setVersion(version);
            }
        } else {
            //It's a new plugin
            plugin = new Plugin();
            plugin.setName(name);
            plugin.setVersion(version);
        }
        plugin.setRecipient(parseStringValue(root.getChild("message-recipient")));
        plugin.setClientPlugin(parseStringValue(root.getChild("client-plugin")));
        plugin.setWsEndpoint(parseStringValue(root.getChild("ws-endpoint")));

        loadMessageTypes(plugin, root);
        loadPages(plugin, root);
        loadExtensionPoints(plugin, root);
        loadExtensions(plugin, root);
        loadActions(plugin, root);
        loadGroups(plugin, root);
        for (Group group : plugin.getGroups()) {
            groups.put(group.getName(), group);
        }
        loadAccessRules(plugin, root);

        PluginHelper.getInstance().savePlugin(plugin);
        return plugin;
    }

    /**
     * Loads actions for specified plug-in.
     *
     * @param plugin plug-in to load actions for
     * @param root element to read actions from.
     */
    private void loadActions(Plugin plugin, Element root) {
        Element actionsTag = root.getChild("actions");
        CollectionLoader<Plugin, Action> actionsLoader = new ActionsLoader(plugin);
        loadCollection(actionsLoader, actionsTag, "action");
    }

    /**
     * Loads extensions for specified plug-in.
     *
     * @param plugin plug-in to load extensions for
     * @param root element to read extensions from.
     */
    private void loadExtensions(Plugin plugin, Element root) {
        Element extensionsTag = root.getChild("extension-points");
        CollectionLoader<Plugin, Extension> extensionsLoader = new ExtensionsLoader(plugin);
        loadCollection(extensionsLoader, extensionsTag, "registerPoint");
    }

    /**
     * Loads e-poins for specified plug-in.
     *
     * @param plugin plug-in to load e-poins for
     * @param root element to read e-poins from.
     */
    private void loadExtensionPoints(Plugin plugin, Element root) {
        Element epointsTag = root.getChild("extension-points");
        CollectionLoader<Plugin, ExtensionPoint> epointsLoader = new ExtensionPointsLoader(plugin);
        loadCollection(epointsLoader, epointsTag, "createPoint");
    }

    /**
     * Loads pages for specified plug-in.
     *
     * @param plugin plug-in to load pages for
     * @param root element to read pages from.
     */
    private void loadPages(Plugin plugin, Element root) {
        Element pagesTag = root.getChild("pages");
        CollectionLoader<Plugin, Page> pagesLoader = new PagesLoader(plugin);
        loadCollection(pagesLoader, pagesTag, "page");
    }

    /**
     * Loads message types for specified plug-in.
     *
     * @param plugin plug-in to load message types for
     * @param root element to read message types from
     */
    private void loadMessageTypes(Plugin plugin, Element root) {
        Element messageTypesTag = root.getChild("message-types");
        CollectionLoader<Plugin, MessageType> messageTypesLoader = new MessageTypesLoader(plugin);
        loadCollection(messageTypesLoader, messageTypesTag, "message-type");
    }

    /**
     * Loads groups for specified plug-in.
     *
     * @param plugin plug-in to load groups for
     * @param root element to read groups from
     */
    private void loadGroups(Plugin plugin, Element root) {
        Element messageTypesTag = root.getChild("groups");
        CollectionLoader<Plugin, Group> groupsLoader = new GroupsLoader(plugin);
        loadCollection(groupsLoader, messageTypesTag, "group");
    }

    /**
     * Loads acess rules for specified plug-in.
     *
     * @param plugin plug-in to load acess rules for
     * @param root element to read acess rules from.
     */
    private void loadAccessRules(Plugin plugin, Element root) {
        Element accessTag = root.getChild("accessRules");
        CollectionLoader<Plugin, AccessRule> rulesLoader = new AccessRulesLoader(plugin, groups);
        loadCollection(rulesLoader, accessTag, "rule");
    }


    /**
     * Loads any collection from children of root element. The elemnts are parsed with loader.
     * Old elements are updated, new added, add element those are not present in description
     * removed.
     *
     * @param loader loader that parses collection's elements.
     * @param collectionElement elemnt that stores collection in description
     * @param itemName name of elemnt in description
     */
    private <T, S> void loadCollection(CollectionLoader<T, S> loader, Element collectionElement, String itemName) {
        List<S> loadedCollection = new ArrayList<S>();
        if (collectionElement != null) {
            List types = collectionElement.getChildren(itemName);
            for (Object item : types) {
                Element element = (Element) item;
                Map<String, String> attributes = new HashMap<String, String>();
                for (Object subItem : element.getAttributes()) {
                    Attribute attrbiute = (Attribute) subItem;
                    attributes.put(attrbiute.getName(), attrbiute.getValue());
                }
                loadedCollection.add(loader.parseAttributes(attributes));
            }
        }
        Set<S> currentCollection = loader.getLoadedCollection();

        Set<S> toBeRemoved = new HashSet<S>();
        for (S item : currentCollection) {
            boolean isPresent = loadedCollection.contains(item);
            if (!isPresent) {
                toBeRemoved.add(item);
            } else {
                loadedCollection.remove(item);
            }
        }

        for (S aToBeRemoved : toBeRemoved) {
            loader.disposeElement(aToBeRemoved);
        }

        for (S aLoadedCollection : loadedCollection) {
            loader.addElement(aLoadedCollection);
        }
    }
}